import sys
import serial
import serial.tools.list_ports
import os
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
                             QHBoxLayout, QLabel, QComboBox, QPushButton,
                             QTextEdit, QGroupBox, QLineEdit)
from PyQt5.QtCore import Qt, QTimer
# from PyQt5.QtGui import QIcon
from openpyxl import Workbook
from datetime import datetime

# ✅ 新增导入：用于实时图表显示
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure

class SerialDebugTool(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("BLE数据采集工具")
        # self.setWindowIcon(QIcon("tool_logo.ico"))
        self.setGeometry(100, 100, 800, 600)

        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        main_layout = QVBoxLayout(central_widget)

        # === 1. 串口配置区 ===
        config_group = QGroupBox("串口配置")
        config_layout = QHBoxLayout(config_group)

        # 1. 端口号标签 + 下拉框
        self.label_port = QLabel("端口号:")
        self.combo_port = QComboBox()
        self.refresh_com_ports()  # 初次加载可用串口
        config_layout.addWidget(self.label_port)
        config_layout.addWidget(self.combo_port)

        # 2. 新增：刷新COM按钮（放在波特率标签之前）
        self.btn_refresh_com = QPushButton("刷新COM")
        self.btn_refresh_com.setStyleSheet("QPushButton { font-size: 14px; padding: 5px; }")
        self.btn_refresh_com.clicked.connect(self.refresh_com_ports)  # 点击后刷新串口列表
        config_layout.addWidget(self.btn_refresh_com)

        # 3. 波特率标签 + 下拉框
        self.label_baud = QLabel("波特率:")
        self.combo_baud = QComboBox()
        self.combo_baud.addItems(["115200", "921600"])
        self.combo_baud.setCurrentText("115200")
        config_layout.addWidget(self.label_baud)
        config_layout.addWidget(self.combo_baud)

        # 4. 打开串口按钮
        self.btn_open_port = QPushButton("打开串口")
        self.btn_open_port.setStyleSheet("QPushButton { font-size: 14px; padding: 5px; }")
        self.btn_open_port.clicked.connect(self.toggle_serial_port)
        config_layout.addWidget(self.btn_open_port)
        
        config_layout.addStretch()

        # === 2. 控制区：步长(m)、步数、文件保存/关闭按钮 ===
        control_group = QGroupBox("文件保存")
        control_layout = QHBoxLayout(control_group)

        control_layout.addWidget(QLabel("步长(m):"))
        self.edit_step = QLineEdit()
        self.edit_step.setPlaceholderText("例如：1.0")
        self.edit_step.setText("1.0")
        self.edit_step.setFixedWidth(60)  # ✅ 控制宽度
        control_layout.addWidget(self.edit_step)

        control_layout.addWidget(QLabel("步数:"))
        self.edit_steps = QLineEdit()
        self.edit_steps.setPlaceholderText("例如：100")
        self.edit_steps.setText("1")
        self.edit_steps.setFixedWidth(60)  # ✅ 控制宽度
        control_layout.addWidget(self.edit_steps)

        # ✅ 文件保存/关闭按钮（初始为“文件保存”）
        self.btn_file_action = QPushButton("文件保存")
        self.btn_file_action.setStyleSheet("QPushButton { font-size: 14px; padding: 5px; }")
        self.btn_file_action.clicked.connect(self.toggle_excel_recording)
        control_layout.addWidget(self.btn_file_action)

        control_layout.addStretch()

        # === 3. 实时显示控制区 === (新增这一部分，放在文件保存和Log输出之间)
        realtime_group = QGroupBox("数据分析")
        realtime_layout = QHBoxLayout(realtime_group)

        # 添加实时显示按钮
        self.btn_realtime_display = QPushButton("打开实时显示")
        self.btn_realtime_display.setStyleSheet("QPushButton { font-size: 14px; padding: 5px; }")
        self.btn_realtime_display.clicked.connect(self.toggle_realtime_display)
        realtime_layout.addWidget(self.btn_realtime_display)

        realtime_layout.addStretch()


        # === 4. Log 显示区 === (原来的第3部分，现在变为第4部分，序号调整)
        log_group = QGroupBox("Log 输出")
        log_layout = QVBoxLayout(log_group)

        self.text_log = QTextEdit()
        self.text_log.setReadOnly(True)
        self.text_log.setPlaceholderText("样机 log 将在此处显示...")
        log_layout.addWidget(self.text_log)

        # === 主布局 ===
        main_layout.addWidget(config_group)
        main_layout.addWidget(control_group)
        main_layout.addWidget(realtime_group)  # 新增的实时显示行
        main_layout.addWidget(log_group)

        # === 串口相关变量 ===
        self.serial_port = None
        self.is_port_open = False
        self.timer = QTimer()
        self.timer.timeout.connect(self.read_serial_data)
        self.timer.setInterval(100)

        # === Excel 相关变量 ===
        self.excel_wb = None
        self.excel_ws = None
        self.excel_row = 1
        self.is_excel_recording = False

        # ✅ 新增：用于生成唯一文件名
        self.file_counter = 0  # y 从 0 开始，每次点击“文件保存”时递增
        self.last_x_value = None  # ✅ 新增：记录上一次的 x（步长*步数），用于判断是否重置 y

        # ✅ 新增：实时显示相关变量
        self.is_realtime_display = False  # 控制是否实时显示数据

        # ✅ 新增：实时显示相关变量
        self.is_realtime_display = False  # 是否开启实时显示
        self.realtime_window = None  # 实时显示的窗口对象
        self.dist_values = []  # 存储所有 Dist 数值
        self.dist_count = 0  # Dist 计数（用作 X 轴）

        # 初始化完成，状态栏显示未连接
        self.statusBar().showMessage("Disconnected")



    def toggle_realtime_display(self):
        if not self.is_realtime_display:
            # 🟢 开启实时显示
            self.is_realtime_display = True
            self.btn_realtime_display.setText("关闭实时显示")

            # 创建并显示实时图表窗口
            self.realtime_window = RealtimeDisplayWindow()
            self.realtime_window.show()

            # 连接信号：当有新的 Dist 数据时，通知实时窗口更新
            self.append_log("✅ 实时显示已开启")
        else:
            # 🔴 关闭实时显示
            self.is_realtime_display = False
            self.btn_realtime_display.setText("打开实时显示")

            # 关闭实时窗口
            if self.realtime_window:
                self.realtime_window.close()
                self.realtime_window = None

            self.append_log("❌ 实时显示已关闭")

    def handle_serial_disconnect(self):
        # 如果当前串口是打开状态，执行关闭操作
        if self.is_port_open:
            self.append_log("⚠️ 检测到串口异常，正在关闭串口连接...")
            self.timer.stop()

            if self.serial_port and self.serial_port.is_open:
                try:
                    self.serial_port.close()
                except Exception as e:
                    self.append_log(f"❌ 关闭串口时出错：{e}")

            # 重置状态
            self.is_port_open = False
            self.serial_port = None

            # 更新 UI 按钮
            self.btn_open_port.setText("打开串口")

            # 更新状态栏
            self.statusBar().showMessage("Disconnected")

            self.append_log("✅ 串口已安全关闭（因异常断开）")

    def refresh_com_ports(self):
        self.combo_port.clear()
        ports = serial.tools.list_ports.comports()
        available_ports = [port.device for port in ports]
        if available_ports:
            self.combo_port.addItems(available_ports)
        else:
            self.combo_port.addItem("未发现可用串口")
            self.combo_port.setEnabled(False)

    def toggle_serial_port(self):
        if not self.is_port_open:
            port_name = self.combo_port.currentText()
            baud_rate = int(self.combo_baud.currentText())

            if port_name == "未发现可用串口":
                self.append_log("❌ 未发现可用串口，请检查连接！")
                return

            try:
                self.serial_port = serial.Serial(
                    port=port_name,
                    baudrate=baud_rate,
                    bytesize=serial.EIGHTBITS,
                    parity=serial.PARITY_NONE,
                    stopbits=serial.STOPBITS_ONE,
                    timeout=1
                )
                self.serial_port.flushInput()
                self.serial_port.flushOutput()

                self.is_port_open = True
                self.btn_open_port.setText("关闭串口")
                self.append_log(f"✅ 串口 {port_name} 已打开，波特率：{baud_rate}")

                self.timer.start()

                # ✅ 状态栏显示已连接
                self.statusBar().showMessage("Connected")

            except Exception as e:
                self.append_log(f"❌ 打开串口失败：{e}")
        else:
            self.timer.stop()
            if self.serial_port and self.serial_port.is_open:
                self.serial_port.close()
            self.is_port_open = False
            self.btn_open_port.setText("打开串口")
            self.append_log("🔒 串口已关闭")
            self.statusBar().showMessage("Disconnected")

    def read_serial_data(self):
        if self.is_port_open and self.serial_port and self.serial_port.is_open:
            try:
                if self.serial_port.in_waiting > 0:
                    data = self.serial_port.readline()
                    text = data.decode('utf-8', errors='ignore').strip()
                    if text:
                        self.append_log(f"← {text}")
                        self.extract_dist_from_raw_text(text)
            except PermissionError as e:
                # 捕获串口被占用/断开时的常见错误，比如拔掉USB串口线
                self.append_log(f"❌ 串口读取异常（可能已断开）：{e}")
                self.handle_serial_disconnect()
            except Exception as e:
                # 捕获其他可能的异常，比如串口硬件错误、被系统关闭等
                self.append_log(f"❌ 读取串口数据出错：{e}")
                self.handle_serial_disconnect()

    def extract_dist_from_raw_text(self, text):
        pos = text.find("Dist: ")
        if pos >= 0:
            remaining = text[pos:]
            parts = remaining.split(": ", 1)
            if len(parts) == 2:
                key = parts[0].strip()
                value_with_unit = parts[1].strip()
                value = value_with_unit.split()[0]
                try:
                    self.write_to_excel(key, value)

                    # ✅ 如果实时显示窗口已开启，则将 Dist 数值传给它
                    if self.is_realtime_display and self.realtime_window:
                        self.realtime_window.add_dist_value(value)

                except Exception as e:
                    self.append_log(f"❌ 写入Excel时出错（提取后）：{e}")

    def write_to_excel(self, key, value):
        if not self.is_excel_recording:
            return
        if self.excel_ws is None:
            return

        try:
            self.excel_ws.cell(row=self.excel_row, column=1, value=key)

            # ✅ 尝试将 value 转为数字（如 "0.07" -> 0.07），如果成功则写入数字，否则保持字符串
            try:
                numeric_value = float(value)  # 尝试转成 float
                self.excel_ws.cell(row=self.excel_row, column=2, value=numeric_value)  # 写入数字
            except ValueError:
                # 如果无法转为数字（比如不是数值字符串），则保持原字符串
                self.excel_ws.cell(row=self.excel_row, column=2, value=value)

            self.excel_row += 1

        except Exception as e:
            self.append_log(f"❌ 写入 Excel 出错：{e}")

    def toggle_excel_recording(self):
        if not self.is_excel_recording:
            # ======================
            # 🟢 开始记录：生成目录与文件
            # ======================

            # 1. 计算 x = 步长 × 步数
            try:
                step_float = float(self.edit_step.text())
                steps_int = int(self.edit_steps.text())
                x = step_float * steps_int
                x = round(x, 1)  # 四舍五入到1位小数，0.6*67 得到 40.2
            except Exception as e:
                self.append_log(f"❌ 计算 x 出错（步长或步数无效）：{e}")
                return

            # ✅ 新增：如果 x（步长*步数）与上次不同，则重置序号 y = 0
            if self.last_x_value != x:
                self.file_counter = 0  # 重置序号从 0 开始
                self.last_x_value = x  # 记录当前 x 值

            # 2. 目录名：BleAcqData_x(m)
            dir_name = f"BleAcqData_{x}(m)"

            # 3. 创建目录（如果不存在）
            os.makedirs(dir_name, exist_ok=True)

            # 4. 文件名：BleAcqData_x(m)_y.xlsx，y 自增
            y = self.file_counter
            self.file_counter += 1  # 下次点击时 y+1
            file_name = f"BleAcqData_{x}(m)_{y}.xlsx"
            file_path = os.path.join(dir_name, file_name)

            # ✅ 修复：将当前文件路径保存到成员变量
            self.excel_filename = file_path

            # 5. 创建并初始化 Excel
            self.excel_wb = Workbook()
            self.excel_ws = self.excel_wb.active
            self.excel_ws.title = "DistData"
            self.excel_row = 1

            # 表头
            self.excel_ws.cell(row=self.excel_row, column=1, value="Key")
            self.excel_ws.cell(row=self.excel_row, column=2, value="Value")
            self.excel_row += 1

            self.is_excel_recording = True
            self.btn_file_action.setText("文件关闭")
            self.append_log(f"✅ 开始记录数据，Excel将保存到：{file_path}")

            # ✅ 状态栏提示：数据保存中...
            self.statusBar().showMessage("数据保存中...")

        else:
            # ======================
            # 🔴 停止记录：保存 Excel
            # ======================
            self.is_excel_recording = False
            try:
                if self.excel_wb:
                    self.excel_wb.save(self.excel_filename)
                    self.excel_wb.close()
                self.btn_file_action.setText("文件保存")
                self.append_log(f"✅ Excel 文件已保存：{self.excel_filename}")

                # ✅ 状态栏提示：数据保存完毕: [文件路径]
                self.statusBar().showMessage(f"数据保存完毕: {self.excel_filename}", 5000)  # 显示5秒

            except Exception as e:
                self.append_log(f"❌ 保存 Excel 失败：{e}")

    def append_log(self, message):
        timestamp = datetime.now().strftime("%H:%M:%S")
        self.text_log.append(f"[{timestamp}] {message}")

    def closeEvent(self, event):
        if self.is_port_open:
            self.timer.stop()
            if self.serial_port and self.serial_port.is_open:
                self.serial_port.close()

        # ✅ 新增：如果正在记录 Excel，保存 Excel 文件
        if self.is_excel_recording:
            self.append_log("⚠️ 检测到正在记录 Excel 数据，正在尝试保存...")
            try:
                if self.excel_wb and self.excel_filename:
                    self.excel_wb.save(self.excel_filename)
                    self.excel_wb.close()
                    self.append_log(f"✅ Excel 文件已自动保存：{self.excel_filename}")
                else:
                    self.append_log("⚠️ Excel 数据尚未初始化或文件路径无效，未保存。")
            except Exception as e:
                self.append_log(f"❌ 保存 Excel 文件时出错：{e}")

        # ✅ 新增：如果实时显示窗口被打开了，也主动关闭它
        if self.realtime_window:
            self.realtime_window.close()  # 关闭实时显示窗口
            self.append_log("✅ 实时显示窗口已关闭")

        # ✅ 允许主窗口关闭
        event.accept()

from matplotlib.ticker import MultipleLocator


class RealtimeDisplayWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("实时距离显示")
        self.setGeometry(400, 50, 1400, 1000)

        layout = QVBoxLayout(self)

        # 创建 matplotlib Figure 和 Canvas
        self.figure = Figure(figsize=(10, 6), dpi=100)
        self.canvas = FigureCanvas(self.figure)
        layout.addWidget(self.canvas)

        # 创建坐标轴
        self.ax = self.figure.add_subplot(111)


        # ✅ 修复中文乱码（关键！）——设置支持中文的字体
        # 方法：指定一个支持中文的字体名称（跨平台尝试）
        # 如果你希望显示中文，取消下面两行的注释，并设置为你系统支持的中文字体
        # 例如：'Microsoft YaHei', 'SimHei', 'Arial Unicode MS', 'PingFang SC' ...
        # font_name = 'Microsoft YaHei'  # Windows
        # font_name = 'Arial Unicode MS'  # macOS
        # font_name = 'WenQuanYi Zen Hei'  # Linux
        # self.ax.set_xlabel('数据点序号 (X轴)', fontsize=10, fontname=font_name)
        # self.ax.set_ylabel('距离 (Dist, 单位：米)', fontsize=10, fontname=font_name)
        # self.ax.set_title('实时距离数据图 (Y轴范围: 0~50)', fontsize=12, fontname=font_name)

        # 如果你暂时不想处理中文字体，可直接使用英文标签（推荐，无乱码）
        # 如下是你原来的英文标签（不会乱码）：
        self.ax.set_xlabel('Index', fontsize=10)
        self.ax.set_ylabel('Distance (m)', fontsize=10)
        self.ax.set_title('Real-time Data', fontsize=14)

        # ✅ 设置 Y 轴刻度间隔为 1
        self.ax.yaxis.set_major_locator(MultipleLocator(1))  # Y轴刻度：每1个单位一格

        # 设置网格
        self.ax.grid(True, linestyle='--', alpha=0.6)

        # 存储数据
        self.dist_values = []  # Y轴：Dist 数值
        self.x_indices = []  # X轴：数据点索引（1, 2, 3...）

        # 绘制初始线条
        self.line, = self.ax.plot([], [], 'b-o', markersize=4)  # 蓝色圆点连线

        # ✅ ===== 重点：设置初始坐标轴范围（即使还没有数据）=====
        # 假设你预计一开始会显示前 10 个点，Y 轴范围是 0 ~ 60 米
        self.ax.set_xlim(0, 10)  # X 轴：初始显示 0~10
        self.ax.set_ylim(0, 60)  # Y 轴：初始显示 0~60 米

        self.canvas.draw()

    def add_dist_value(self, dist_value):
        """从外部调用，添加一个新 Dist 数值"""
        try:
            dist_float = float(dist_value)
        except ValueError:
            print(f"[警告] 无法解析 Dist 值: '{dist_value}'，已跳过")
            return

        self.dist_values.append(dist_float)
        self.x_indices.append(len(self.x_indices) + 1)  # X轴从1开始

        # 更新曲线数据
        self.line.set_data(self.x_indices, self.dist_values)

        # 动态调整 X 轴范围：至少显示所有数据，或限制范围
        if len(self.x_indices) > 0:
            self.ax.set_xlim(0, max(10, self.x_indices[-1] + 1))  # X轴范围自适应

        # Y轴固定范围 0~50
        self.ax.set_ylim(0, 60)

        # 重绘
        self.canvas.draw()

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = SerialDebugTool()
    window.show()
    sys.exit(app.exec_())